#! /usr/bin/python -W ignore

# mythcal

# Copyright 2009-2014 Richard Fearn
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

CONFIG_FILE = "mythcal.conf"
CACHE_FILE = "mythcal.cache"
CREDENTIALS_FILE = "mythcal.credentials"

VERSION = "0.27.0"

# From https://developers.google.com/accounts/docs/OAuth2:
#     "...a client secret, which you embed in the source code of your
#     application. (In this context, the client secret is obviously not treated
#     as a secret.)"
OAUTH_CLIENT_ID = '17959633059-62g87ggkkr0a1lub43egv3rgfp7afphq.apps.googleusercontent.com'
OAUTH_CLIENT_SECRET = 'wlkk_7uk8La2Ceqp9OY49qSH'
OAUTH_SCOPE = 'https://www.googleapis.com/auth/calendar'
OAUTH_REDIRECT_URI = 'urn:ietf:wg:oauth:2.0:oob'

AUTH_MESSAGE_1 = """Visit this URL in your browser:

  %s

When you have granted mythcal access to your calendar, enter the code you are
given below.
"""

AUTH_MESSAGE_2 = "Done! You can now run mythcal without the `--auth` flag."

DATE_FORMAT = "%Y-%m-%d"
DATE_TIME_FORMAT = "%Y-%m-%dT%H:%M:%S.000%z"

import ConfigParser
from optparse import Values, OptionParser

from datetime import datetime
import pickle
import os
import sys

from MythTV import MythBE
import pytz
import time

from oauth2client.client import OAuth2WebServerFlow
from oauth2client.file import Storage

import httplib2
from apiclient.discovery import build
from oauth2client.file import Storage
from apiclient.errors import HttpError

config = ConfigParser.RawConfigParser()
config.read(CONFIG_FILE)

settings = Values({
        "mythtv": Values({
                "timezone": config.get("mythtv", "timezone")
        }),
        "calendar": Values({
                "id": config.get("calendar", "id"),
        })
})

parser = OptionParser()
parser.add_option("-a", "--auth",    action="store_true", dest="auth",    default=False, help="authenticate (required before using mythcal for the first time)")
parser.add_option("-n", "--dry-run", action="store_true", dest="dry_run", default=False, help="perform a trial run; don't make any changes (implies -v)")
parser.add_option("-V", "--version", action="store_true", dest="version", default=False, help="show version and exit")
parser.add_option("-v", "--verbose", action="store_true", dest="verbose", default=False, help="increase verbosity")
(options, args) = parser.parse_args()

if options.version:
    print "mythcal %s" % VERSION
    sys.exit(0)

if options.dry_run:
    options.verbose = True

if options.auth:

    flow = OAuth2WebServerFlow(
        client_id = OAUTH_CLIENT_ID,
        client_secret = OAUTH_CLIENT_SECRET,
        scope = OAUTH_SCOPE,
        redirect_uri = OAUTH_REDIRECT_URI
    )

    auth_uri = flow.step1_get_authorize_url()
    print AUTH_MESSAGE_1 % auth_uri

    auth_code = raw_input("Code: ")
    print

    credentials = flow.step2_exchange(auth_code)
    storage = Storage(CREDENTIALS_FILE)
    storage.put(credentials)
    print AUTH_MESSAGE_2
    sys.exit(0)

# check for existence of credentials
if not os.path.isfile(CREDENTIALS_FILE):
    print >>sys.stderr, "mythcal: no credentials file. You need to run `mythcal --auth` first"
    sys.exit(1)

# get pytz timezone object for local time zone
if settings.mythtv.timezone not in pytz.all_timezones:
    print >>sys.stderr, "mythcal: timezone name '%s' is not recognised" % settings.mythtv.timezone
    sys.exit(1)
time_zone = pytz.timezone(settings.mythtv.timezone)

DATE_TIME_DEBUG = False

def mythtv_time_to_naive_utc_time(mythtv_time):

    """Convert MythTV time to naive UTC time"""

    if DATE_TIME_DEBUG:
        print "converting time..."

    # The timestamp we get from a MythTV datetime object is affected by its timezone...
    aware_local_mythtv_time = mythtv_time
    if DATE_TIME_DEBUG:
        print "  %s\t%s\t%d" % (aware_local_mythtv_time, aware_local_mythtv_time.timetuple(), aware_local_mythtv_time.timestamp())

    # ...so we convert to UTC to ensure we get a POSIX timestamp from it
    aware_utc_mythtv_time = aware_local_mythtv_time.astimezone(pytz.utc)
    if DATE_TIME_DEBUG:
        print "  %s\t%s\t%d" % (aware_utc_mythtv_time, aware_utc_mythtv_time.timetuple(), aware_utc_mythtv_time.timestamp())

    # Create a standard datetime that represents the same point in time...
    aware_utc_datetime = datetime.fromtimestamp(aware_utc_mythtv_time.timestamp(), pytz.utc)
    if DATE_TIME_DEBUG:
        print "  %s\t%s" % (aware_utc_datetime, aware_utc_datetime.timetuple())

    # ...and drop the timezone
    naive_utc_datetime = aware_utc_datetime.replace(tzinfo=None)
    if DATE_TIME_DEBUG:
        print "  %s\t\t%s" % (naive_utc_datetime, naive_utc_datetime.timetuple())

    return naive_utc_datetime

def convert_program(prog):
    """Converts a MythTV Program object to a dictionary"""
    return {
        "title": prog.title,
        "subtitle": prog.subtitle,
        "channel": prog.channame,
        "start": int(prog.starttime.timestamp()),
        "end": int(prog.endtime.timestamp()),
        "description": prog.description
    }

def sort_programs_by_start(p1, p2):
    return cmp(p1["start"], p2["start"])

def get_recordings_from_backend():
    """Gets current and upcoming recordings from MythTV"""

    if options.verbose:
        print "Getting recordings from MythTV backend..."

    mythtv = MythBE()

    upcoming = mythtv.getUpcomingRecordings()
    upcoming = list(upcoming) # convert listiterator to list

    if options.verbose:
        print "    found %d upcoming recording(s)" % len(upcoming)

    upcoming = map(convert_program, upcoming)
    upcoming.sort(sort_programs_by_start)

    current = []
    for recorder in mythtv.getRecorderList():
        # str(...) required due to MythTV issue #7648
        # see http://svn.mythtv.org/trac/ticket/7648
        if mythtv.isRecording(str(recorder)):
            prog = mythtv.getCurrentRecording(str(recorder))
            # don't include live programmes (issue 16)
            if prog.get("recgroup") != "LiveTV":
                current.append(prog)
    if options.verbose:
        print "    found %d current recording(s)" % len(current)

    current = map(convert_program, current)
    current.sort(sort_programs_by_start)

    return {"current": current, "future": upcoming}

# get recordings from MythTV backend
recordings = get_recordings_from_backend()

# load recording list from last time
last_recordings = None
if os.path.exists(CACHE_FILE):
    if options.verbose:
        print "Reading cache file..."
    f = open(CACHE_FILE, "r")
    last_recordings = pickle.load(f)
    f.close()
    if options.verbose:
        print "Done."
else:
    if options.verbose:
        print "Cache file does not exist."

def submit_batch_request(request, url):
    response_feed = calendar_service.ExecuteBatch(request, url)
    # for entry in response_feed.entry:
    #     print "%s; status %s; reason %s" % (entry.batch_id.text, entry.batch_status.code, entry.batch_status.reason)

def delete_existing_events():
    """Deletes all events from the calendar"""

    if options.verbose:
        print "Deleting existing entries..."
    result = service.events().list(calendarId = settings.calendar.id).execute()
    while result:
        for item in result["items"]:
            if options.verbose:
                print "    will delete \"%s\"" % item["summary"]
            if not options.dry_run:
                service.events().delete(calendarId = settings.calendar.id, eventId = item["id"]).execute()
        if "nextPageToken" in result:
            result = service.events().list(calendarId = settings.calendar.id, pageToken = result["nextPageToken"]).execute()
        else:
            result = None
    if options.verbose:
        print "Existing entries deleted."

# update calendar, and output new recording list, if different
if recordings != last_recordings:

    # get calendar service and log in
    if options.verbose:
        print "Logging into Google Calendar..."
    storage = Storage(CREDENTIALS_FILE)
    credentials = storage.get()
    http = httplib2.Http()
    http = credentials.authorize(http)
    service = build("calendar", "v3", http=http)
    if options.verbose:
        print "Done."

    # Get all calendars
    if options.verbose:
        print "Getting calendar..."
    try:
        result = service.calendars().get(calendarId = settings.calendar.id).execute()
    except HttpError, e:
        print >>sys.stderr, "mythcal: could not get calendar with id '%s':" % settings.calendar.id
        print >>sys.stderr, e.content
        sys.exit(1)
    if options.verbose:
        print "Done."

    delete_existing_events()

    def create_all_day_event(title, start, end, content=None):
        event = {
            "summary": title,
            "start": {
                "date": time.strftime(DATE_FORMAT, start),
            },
            "end": {
                "date": time.strftime(DATE_FORMAT, end),
            },
            "description": content
        }
        return event

    def create_programme_event(title, subtitle, channel, start, end, content=None):
        if subtitle:
            event_title = "%s: %s (%s)" % (title, subtitle, channel)
        else:
            event_title = "%s (%s)" % (title, channel)
        event = {
            "summary": event_title,
            "start": {
                "dateTime": datetime.fromtimestamp(start, tz=pytz.utc).strftime(DATE_TIME_FORMAT),
            },
            "end": {
                "dateTime": datetime.fromtimestamp(end, tz=pytz.utc).strftime(DATE_TIME_FORMAT),
            },
            "description": content
        }
        return event

    if options.verbose:
        print "Adding new entries..."

    # add an event for current/future recordings
    for prog in recordings["current"] + recordings["future"]:
        if options.verbose:
            print "    will add \"%s\"" % prog["title"]
        if not options.dry_run:
            event = create_programme_event(prog["title"], prog["subtitle"], prog["channel"], prog["start"], prog["end"], prog["description"])
            service.events().insert(calendarId = settings.calendar.id, body=event).execute()

    # add 'last updated' event
    last_update_text = "MythTV updated %s" % time.strftime("%H:%M", time.localtime())
    if options.verbose:
        print "    will add \"%s\"" % last_update_text
    if not options.dry_run:
        event = create_all_day_event(title=last_update_text, start=time.gmtime(), end=time.gmtime(time.time() + 24*60*60))
        service.events().insert(calendarId = settings.calendar.id, body=event).execute()

    if options.verbose:
        print "New entries added."

    # update last recording list
    if options.verbose:
        print "Updating cache..."
    if not options.dry_run:
        f = open(CACHE_FILE, "w")
        pickle.dump(recordings, f)
        f.close()

    if options.verbose:
        print "Done."

else:
    if options.verbose:
        print "Recordings have not changed; not updating calendar."

if options.verbose:
    print "Finished."
